/* Copyright (C) 2001, 2008 United States Government as represented by
the Administrator of the National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind.data;

import gov.nasa.worldwind.avlist.*;
import gov.nasa.worldwind.cache.MemoryCache;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.terrain.ElevationModelConfiguration;
import gov.nasa.worldwind.util.*;
import org.w3c.dom.Document;

import java.io.IOException;

/**
 * @author dcollins
 * @version $Id: TiledElevationProducer.java 13238 2010-03-22 21:42:04Z dcollins $
 */
public class TiledElevationProducer extends TiledRasterProducer
{
    private static final String DEFAULT_IMAGE_FORMAT = "application/bil32";
    private static final double DEFAULT_MISSING_DATA_SIGNAL = (double) Short.MIN_VALUE;
    // Statically reference the readers used to for unknown data sources. This drastically improves the performance of
    // reading large quantities of sources. Since the readers are invoked from a single thread, they can be
    // safely re-used.
    private static DataRasterReader[] readers = new DataRasterReader[]
        {
            new BILRasterReader(),
            new GeotiffRasterReader()
        };

    public TiledElevationProducer(MemoryCache cache, int writeThreadPoolSize)
    {
        super(cache, writeThreadPoolSize);
    }

    public TiledElevationProducer()
    {
    }

    public String getDataSourceDescription()
    {
        StringBuilder sb = new StringBuilder();
        sb.append(Logging.getMessage("TiledElevationProducer.Description"));
        sb.append(" (").append(super.getDataSourceDescription()).append(")");
        return sb.toString();
    }

    protected DataRaster createDataRaster(int width, int height, Sector sector, AVList params)
    {
        // Create a BIL elevation raster to hold the tile's data.
        AVList bufferParams = new AVListImpl();

        // ByteBufferRaster is expected buffer type in "DATA_TYPE" key,
        // but WorldFile specifies data type in the "PIXEL_TYPE" key.
        bufferParams.setValue(AVKey.DATA_TYPE, params.getValue(AVKey.PIXEL_TYPE));
        bufferParams.setValue(AVKey.BYTE_ORDER, params.getValue(AVKey.BYTE_ORDER));
        ByteBufferRaster bufferRaster = new ByteBufferRaster(width, height, sector, bufferParams);

        // Clear the raster with the missing data replacment.
        // This code expects the string "gov.nasa.worldwind.avkey.MissingDataValue", which now corresponds to the key 
        // MISSING_DATA_REPLACEMENT.
        Object o = params.getValue(AVKey.MISSING_DATA_REPLACEMENT);
        if (o != null && o instanceof Double)
        {
            Double missingDataValue = (Double) o;
            bufferRaster.fill(missingDataValue);
            bufferRaster.setTransparentValue(missingDataValue);
        }

        return bufferRaster;
    }

    protected DataRasterReader[] getDataRasterReaders()
    {
        return readers;
    }

    protected DataRasterWriter[] getDataRasterWriters()
    {
        return new DataRasterWriter[]
            {
                new BILRasterWriter()
            };
    }

    protected String validateDataSource(DataSource dataSource)
    {
        // TiledElevationProducer does not accept null data sources.
        if (dataSource == null)
            return Logging.getMessage("nullValue.DataSourceIsNull");

        // TiledElevationProducer accepts BufferedDataRaster as a data source. If the data source is a DataRaster, then
        // check that it's a BufferedDataRaster.
        if (dataSource.getSource() instanceof DataRaster)
        {
            DataRaster raster = (DataRaster) dataSource.getSource();

            if (!(raster instanceof BufferedDataRaster))
                return Logging.getMessage("TiledRasterProducer.UnrecognizedDataSource", raster);

            String s = this.validateDataSourceParams(raster, String.valueOf(raster));
            if (s != null)
                return s;
        }
        // For any other data source, attempt to find a reader for the data source. If the reader know's the data
        // source's raster type, then check that it's elevation data.
        else
        {
            DataRasterReader reader = ReadableDataRaster.findReaderFor(dataSource, this.getDataRasterReaders());
            if (reader == null)
                return Logging.getMessage("TiledRasterProducer.UnrecognizedDataSource", dataSource.getSource());

            AVList params = new AVListImpl();
            try
            {
                reader.readMetadata(dataSource, params);
            }
            catch (IOException e)
            {
                return Logging.getMessage("TiledRasterProducer.ExceptionWhileReading", dataSource.getSource(),
                    e.getMessage());
            }

            String s = this.validateDataSourceParams(params, String.valueOf(dataSource.getSource()));
            if (s != null)
                return s;
        }

        return null;
    }

    protected String validateDataSourceParams(AVList params, String name)
    {
        if (params.hasKey(AVKey.RASTER_TYPE) &&
            params.getValue(AVKey.RASTER_TYPE) != AVKey.RASTER_TYPE_ELEVATION)
        {
            return Logging.getMessage("TiledRasterProducer.UnrecognizedRasterType",
                params.getValue(AVKey.RASTER_TYPE), name);
        }

        if (params.hasKey(AVKey.COORDINATE_SYSTEM) &&
            params.getValue(AVKey.COORDINATE_SYSTEM) != AVKey.COORDINATE_SYSTEM_GEOGRAPHIC)
        {
            return Logging.getMessage("TiledRasterProducer.UnrecognizedCoordinateSystem",
                params.getValue(AVKey.COORDINATE_SYSTEM), name);
        }

        if (params.hasKey(AVKey.ELEVATION_UNIT) &&
            params.getValue(AVKey.ELEVATION_UNIT) != AVKey.ELEVATION_UNIT_METER)
        {
            return Logging.getMessage("TiledElevationProducer.UnrecognizedElevationUnit",
                params.getValue(AVKey.ELEVATION_UNIT), name);
        }

        if (params.getValue(AVKey.SECTOR) == null)
            return Logging.getMessage("TiledRasterProducer.NoSector", name);

        return null;
    }

    protected void initProductionParameters(AVList params)
    {
        // Preserve backward compatibility with previous verisons of TiledElevationProducer. If the caller specified a
        // format suffix parameter, use it to compute the image format properties. This gives priority to the format
        // suffix property to ensure applications which use format suffix continue to work.
        if (params.getValue(AVKey.FORMAT_SUFFIX) != null)
        {
            String s = WWIO.makeMimeTypeForSuffix(params.getValue(AVKey.FORMAT_SUFFIX).toString());
            if (s != null)
            {
                params.setValue(AVKey.IMAGE_FORMAT, s);
                params.setValue(AVKey.AVAILABLE_IMAGE_FORMATS, new String[] {s});
            }
        }

        // Use the default image format if none exists.
        if (params.getValue(AVKey.IMAGE_FORMAT) == null)
            params.setValue(AVKey.IMAGE_FORMAT, DEFAULT_IMAGE_FORMAT);

        // Compute the available image formats if none exists.
        if (params.getValue(AVKey.AVAILABLE_IMAGE_FORMATS) == null)
        {
            params.setValue(AVKey.AVAILABLE_IMAGE_FORMATS,
                new String[] {params.getValue(AVKey.IMAGE_FORMAT).toString()});
        }

        // Compute the format suffix if none exists.        
        if (params.getValue(AVKey.FORMAT_SUFFIX) == null)
        {
            params.setValue(AVKey.FORMAT_SUFFIX,
                WWIO.makeSuffixForMimeType(params.getValue(AVKey.IMAGE_FORMAT).toString()));
        }

        // Compute the pixel type from the image format.
        if (params.getValue(AVKey.PIXEL_TYPE) == null && params.getValue(AVKey.IMAGE_FORMAT) != null)
        {
            String s = WWIO.makePixelTypeForMimeType(params.getValue(AVKey.IMAGE_FORMAT).toString());
            if (s != null)
                params.setValue(AVKey.PIXEL_TYPE, s);
        }

        // Use the default pixel type if none exists.
        if (params.getValue(AVKey.PIXEL_TYPE) == null)
            params.setValue(AVKey.PIXEL_TYPE, AVKey.INT16);

        // Use the default byte order if none exists.
        if (params.getValue(AVKey.BYTE_ORDER) == null)
            params.setValue(AVKey.BYTE_ORDER, AVKey.LITTLE_ENDIAN);

        // This code expects the string "gov.nasa.worldwind.avkey.MissingDataValue", which now corresponds to the key
        // MISSING_DATA_REPLACEMENT.
        if (params.getValue(AVKey.MISSING_DATA_REPLACEMENT) == null)
            params.setValue(AVKey.MISSING_DATA_REPLACEMENT, DEFAULT_MISSING_DATA_SIGNAL);
    }

    protected LatLon computeRasterTileDelta(int tileWidth, int tileHeight, Iterable<? extends DataRaster> rasters)
    {
        LatLon pixelSize = this.computeSmallestPixelSize(rasters);
        // Compute the tile size in latitude and longitude, given a raster's sector and dimension, and the tile
        // dimensions. In this computation a pixel is assumed to have no dimension. We measure the distance between
        // pixels rather than some pixel dimension.
        double latDelta = (tileHeight - 1) * pixelSize.getLatitude().degrees;
        double lonDelta = (tileWidth - 1) * pixelSize.getLongitude().degrees;
        return LatLon.fromDegrees(latDelta, lonDelta);
    }

    /**
     * Returns an ElevationModel configuration document which describes the tiled elevation data produced by this
     * TiledElevationProducer. The document's contents are based on the configuration document for a basic
     * ElevationModel, except this document describes an offline dataset. This returns null if the parameter list is
     * null, or if the configuration document cannot be created for any reason.
     *
     * @param params the parameters which describe an ElevationModel configuration document's contents.
     *
     * @return the configuration document, or null if the parameter list is null or does not contain the required
     *         parameters.
     */
    protected Document createConfigDoc(AVList params)
    {
        AVList configParams = params.copy();

        // Determine a default display name if none exists.
        if (configParams.getValue(AVKey.DISPLAY_NAME) == null)
            configParams.setValue(AVKey.DISPLAY_NAME, params.getValue(AVKey.DATASET_NAME));

        // Set the SERVICE_NAME and NETWORK_RETRIEVAL_ENABLED parameters to indicate this dataset is offline.
        configParams.setValue(AVKey.SERVICE_NAME, "Offline");
        configParams.setValue(AVKey.NETWORK_RETRIEVAL_ENABLED, Boolean.FALSE);

        // TiledElevationProducer and the DataRaster classes use MISSING_DATA_REPLACEMENT to denote the missing data
        // value, whereas all other WWWJ code uses MISSING_DATA_SIGNAL. Replace the MISSING_DATA_REPLACEMENT with the
        // MISSING_DATA_SIGNAL here to ensure this discrapancy is limited to data installation code.
        configParams.removeKey(AVKey.MISSING_DATA_REPLACEMENT);
        configParams.setValue(AVKey.MISSING_DATA_SIGNAL, params.getValue(AVKey.MISSING_DATA_REPLACEMENT));

        // Return a configuration file for a BasicElevationModel. BasicElevationModel is the standard WWJ component
        // which consumes tiled elevation data.
        return ElevationModelConfiguration.createBasicElevationModelDocument(configParams);
    }
}
